<?php
/**
 * 分析路由，处理请求
 */
declare(strict_types=1);

namespace Libraries;

use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Server\Task;
use Swoole\Coroutine;
use Swoole\Coroutine\WaitGroup;
use Libraries\Bases\ControllerBase;
use Libraries\Bases\HookBase;
use ErrorException;

class Dispatcher
{
    private static $arrPathCache = []; // 路由缓存
    private static $arrHookConfig = []; // Hook配置
    
    const HOOK_BEFORE = 'before';
    const HOOK_AFTER = 'after';
    
    /**
     * 分析路由，并返回至页面
     * @param Request $objRequest
     * @param Response $objResponse
     * @return void
     */
    public static function handleRequest(Request $objRequest, Response $objResponse): void
    {
        if ($objRequest->server['path_info'] == '/favicon.ico') {
            self::endWith404($objResponse, false);
            return;
        }
        $objCtrl = null;
        $strPath = hash('md5', $objRequest->server['path_info']);
        $blnWithCache = false;
        $strCtrlName = $strM = $strC = $strA = '';
        if (isset(self::$arrPathCache[$strPath])) {
            [$strCtrlName, $strM, $strC, $strA, $strExt, $arrParam] = self::$arrPathCache[$strPath];
            $objCtrl = new $strCtrlName($objRequest, $objResponse);
            $blnWithCache = true;
        } else {
            [$arrRoutes, $arrParam, $strExt] = self::getPathMore($objRequest->server['path_info']);
            foreach ($arrRoutes as $arrRoute) {
                [$strM, $strC, $strA] = $arrRoute;
                $strM = (($strM == 'index' || $strM == '') ? '' : ucfirst($strM) . '\\');
                $strC != '' || $strC = 'index';
                $strCtrlName = '\\Apps\\' . APP_NAME . '\\Controllers\\' . $strM . ucfirst($strC);
                if (!class_exists($strCtrlName)) {
                    continue;
                }
                if (is_callable([$strCtrlName, $strA])) {
                    $objCtrl = new $strCtrlName($objRequest, $objResponse);
                    break;
                } elseif (is_callable([$strCtrlName, '__remap'])) {
                    $strA = '__remap';
                    $objCtrl = new $strCtrlName($objRequest, $objResponse);
                    break;
                }
            }
            unset($arrRoutes, $arrRoute);
        }
        if (!$objCtrl) {
            self::endWith404($objResponse);
            return;
        }
        $objCtrl->strExt = $strExt;
        self::runHook($objCtrl, $strA, self::HOOK_BEFORE, $arrParam); // Hook before
        if ($objCtrl->blnOutputEnd != true) {
            call_user_func_array([$objCtrl, $strA], $arrParam);
        }
        self::runHook($objCtrl, $strA, self::HOOK_AFTER, $arrParam); // Hook after
        $blnWithCache || self::$arrPathCache[$strPath] = [$strCtrlName, $strM, $strC, $strA, $strExt, $arrParam];
        unset($objCtrl, $strCtrlName, $strM, $strC, $strA, $blnWithCache, $strPath, $strExt, $arrParam);
    }

    /**
     * 输出404
     * @param Response $objResponse
     * @param bool $blnRedirect
     */
    public static function endWith404(Response $objResponse, bool $blnRedirect = true): void
    {
        $objResponse->status(404);
        if ($blnRedirect) {
            $strPage404 = Config::get('err_pages:404');
            $strPage404 && $objResponse->redirect($strPage404);
        }
    }

    /**
     * 分析路由
     * @param string $strUrlPath
     * @return array
     */
    private static function getPathMore(string $strUrlPath): array
    {
        $strUrlPath = trim($strUrlPath, '/');
        /* 地址后缀 */
        $arrPathInfo = pathinfo($strUrlPath);
        $strExt = $arrPathInfo['extension'] ?? '';
        if (strlen($strExt)) {
            $strUrlPath = substr($strUrlPath, 0, (-1 - strlen($strExt)));
        }
        unset($arrPathInfo);
        /* 以“/”分隔 */
        $arrParam = [];
        $arrUrlPath = explode('/', $strUrlPath);
        if (count($arrUrlPath) > 3) {
            $arrParam = array_slice($arrUrlPath, 3);
            $arrUrlPath = array_slice($arrUrlPath, 0, 3);
        } else {
            $arrUrlPath = array_pad($arrUrlPath, 3, 'index');
        }
        /* 可能的结果 */
        return [
            [
                [$arrUrlPath[0], $arrUrlPath[1], $arrUrlPath[2]],
                [$arrUrlPath[0], 'index', $arrUrlPath[1]],
                ['', $arrUrlPath[0], $arrUrlPath[1]],
                ['', 'index', $arrUrlPath[0]],
            ],
            $arrParam,
            $strExt,
        ];
    }

    /**
     * 按类型获取Hook配置
     * @param string $strHookType
     * @return array
     */
    private static function getHookConfig(string $strHookType): array
    {
        self::$arrHookConfig || self::$arrHookConfig = Config::get('hooks');
        return self::$arrHookConfig[$strHookType] ?? [];
    }
    
    /**
     * HOOK运行
     * @param ControllerBase $objCtrl
     * @param string $strA
     * @param string $strHookType
     * @param array $arrParam
     * @return void
     */
    private static function runHook(
        ControllerBase $objCtrl,
        string $strA,
        string $strHookType,
        array $arrParam = []
    ): void {
        $arrHook = self::getHookConfig($strHookType);
        $skipHooks = $objCtrl->skipHooks ?? [];
        if ($skipHooks == '*' || !$arrHook || !is_array($arrHook)) {
            return;
        }
        $strClass = '\\Apps\\' . APP_NAME . '\\' . 'Hooks' . '\\';
        $objWG = null;
        foreach ($arrHook as $strHook) {
            $strHookClass = $strClass . ucfirst($strHook);
            if (!class_exists($strHookClass)) {
                continue;
            }
            if (is_array($skipHooks) && isset($skipHooks[$strHook])) {
                if ($skipHooks[$strHook] === '*') {
                    continue;
                }
                if (is_array($skipHooks[$strHook]) && in_array($strA, $skipHooks[$strHook], true)) {
                    continue;
                }
            }
            $objWG || $objWG = new WaitGroup();
            $objWG->add();
            Coroutine::create(function () use ($strHookClass, $objCtrl, $strA, $strHookType, $arrParam, $objWG) {
                $objHook = new $strHookClass($objCtrl);
                if ($objHook instanceof HookBase) {
                    $objHook->run($strA, $strHookType, $arrParam);
                }
                unset($objHook);
                $objWG->done();
            });
        }
        $objWG && $objWG->wait();
    }
    
    /**
     * 取已缓存的路由
     * @return array
     */
    public static function getCachedPaths(): array
    {
        return self::$arrPathCache;
    }
    
    /**
     * 处理Task请求
     * @param Task $objTask
     * @return void
     * @throws ErrorException
     */
    public static function handleTask(Task $objTask): void
    {
        self::handleTaskFinish($objTask->data, -1, $objTask);
    }

    /**
     * 处理TaskFinish
     *
     * @param array $arrData
     * @param int $intTaskId
     * @param null $objTask
     * @throws ErrorException
     */
    public static function handleTaskFinish(array $arrData, int $intTaskId = -1, $objTask = null): void
    {
        if (!is_array($arrData) || !isset($arrData[0]) || !is_array($arrData[0]) || count($arrData[0]) != 2
            || !isset($arrData[0][0]) || !isset($arrData[0][1])) {
            throw new ErrorException('错误的参数！请这样：task/finish([[类名, 方法名], [参数...]]);');
        }
        if (!isset($arrData[1]) || !is_array($arrData[1])) {
            $arrData[1] = [];
        }
        $intTaskId >= 0 && $arrData[1][] = $intTaskId;
        $strClsName = '\\Apps\\' . APP_NAME . '\\Tasks\\' . $arrData[0][0];
        $strMethod = strval($arrData[0][1]);
        if (class_exists($strClsName) && is_callable([$strClsName, $strMethod])) {
            $objCls = new $strClsName($objTask);
            call_user_func_array([$objCls, $strMethod], $arrData[1]);
        } else {
            throw new ErrorException('找不到 ' . $strClsName . '::' . $strMethod . '()');
        }
        unset($strClsName, $arrData, $objCls, $strMethod, $intTaskId);
    }
    
    /**
     * 处理TCP收到的请求
     * @param array $arrData
     * @param int $intFd
     * @param int $intReactorId
     * @return void
     * @throws ErrorException
     */
    public static function handleTcpRecv(array $arrData, int $intFd, int $intReactorId = -1): void
    {
        if (!is_array($arrData) || !isset($arrData[0]) || !is_array($arrData[0]) || count($arrData[0]) != 2
            || !isset($arrData[0][0]) || !isset($arrData[0][1])) {
            throw new ErrorException('错误的参数！请这样：request([[类名, 方法名], [参数...]]);');
        }
        if (!isset($arrData[1]) || !is_array($arrData[1])) {
            $arrData[1] = [];
        }
        $strClsName = '\\Apps\\' . APP_NAME . '\\Services\\' . $arrData[0][0];
        $strMethod = strval($arrData[0][1]);
        if (class_exists($strClsName) && is_callable([$strClsName, $strMethod])) {
            $objCls = new $strClsName($intFd, $intReactorId);
            call_user_func_array([$objCls, $strMethod], $arrData[1]);
        } else {
            throw new ErrorException('找不到 ' . $strClsName . '::' . $strMethod . '()');
        }
        unset($strClsName, $arrData, $objCls, $strMethod, $intFd, $intReactorId);
    }
}
